/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.util;

import java.io.File;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;

import fr.x9c.cadmium.kernel.Fatal;

/**
 * This class provides some utility methods about character, string and
 * integer conversions. <br/>
 * Character and string conversion are done using the <tt>ISO-8859-1</tt>
 * charset (<i>a.k.a.</i> <tt>ISO-Latin-1</tt>)  that is used by Objective
 * Caml.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.0
 * @since 1.0
 */
public final class Misc {

    /** Value for read permission. */
    private static final int READ_PERM = 4;

    /** Value for write permission. */
    private static final int WRITE_PERM = 2;

    /** Value for execute permission. */
    private static final int EXECUTE_PERM = 1;

    /** Byte cardinality, that is number of possible byte values. */
    private static final int BYTE_CARDINAL =
        Byte.MAX_VALUE - Byte.MIN_VALUE + 1;

    /** Short cardinality, that is number of possible short values. */
    private static final int SHORT_CARDINAL =
        Short.MAX_VALUE - Short.MIN_VALUE + 1;

    /** Integer cardinality, that is number of possible integer values. */
    private static final long INTEGER_CARDINAL =
        ((long) Integer.MAX_VALUE) - ((long) Integer.MIN_VALUE) + 1;

    /** Charset used in bytecode files. */
    private static final Charset CHARSET = Charset.forName("ISO-8859-1");

    /**
     * Conversion from byte to char, using {@link #CHARSET}
     * (<i>i.e.</i> <tt>ISO-8859-1</tt> / <tt>ISO-Latin-1</tt>). <br/>
     * This table is constructed fom {@link java.nio.charset.Charset} class and
     * is used to increase conversion speed.
     */
    private static final char[] BYTE_TO_CHAR;

    /**
     * Conversion from char to byte, using {@link #CHARSET}
     * (<i>i.e.</i> <tt>ISO-8859-1</tt> / <tt>ISO-Latin-1</tt>). <br/>
     * This table is constructed fom {@link java.nio.charset.Charset} class and
     * is used to increase conversion speed.
     */
    private static final byte[] CHAR_TO_BYTE;

    /* Initialization of conversion tables. */
    static {
        final byte[] ba = new byte[Byte.MAX_VALUE - Byte.MIN_VALUE + 1];
        for (int i = Byte.MIN_VALUE; i <= Byte.MAX_VALUE; i++) {
            ba[i - Byte.MIN_VALUE] = (byte) i;
        } // end for
        BYTE_TO_CHAR = Misc.CHARSET.decode(ByteBuffer.wrap(ba)).array();
        assert ba.length == Misc.BYTE_TO_CHAR.length : "invalid BYTE_TO_CHAR";
        final char[] ca =
            new char[Character.MAX_VALUE - Character.MIN_VALUE + 1];
        for (int i = Character.MIN_VALUE; i <= Character.MAX_VALUE; i++) {
            ca[i - Character.MIN_VALUE] = (char) i;
        } // end for
        CHAR_TO_BYTE = Misc.CHARSET.encode(CharBuffer.wrap(ca)).array();
        assert ca.length == Misc.CHAR_TO_BYTE.length : "invalid CHAR_TO_BYTE";
    } // end static block

    /**
     * No instance of this class.
     */
    private Misc() {
    } // end empty constructor

    /**
     * Interprets the bits of an <tt>int</tt> as an unsigned 32-bit value. <br/>
     * Opposite of {@link #unsignedToSigned(long)}.
     * @param x bits to convert
     * @return the unsigned 32-bit value of <i>x</i>
     * @see #unsignedToSigned(long)
     */
    public static long signedToUnsigned(final int x) {
        return x >= 0 ? x : Misc.INTEGER_CARDINAL + x;
    } // end method 'signedToUnsigned(int)'

    /**
     * Interprets the 32-bit unsigned value of a <tt>long</tt> as
     * a signed 32-bit value. <br/>
     * Opposite of {@link #signedToUnsigned(int)}.
     * @param x bits to convert - should be in <tt>0..{@link #INTEGER_CARDINAL}</tt>
     * @return the signed 32-bit value of <i>x</i>
     * @see #signedToUnsigned(int)
     * @see #INTEGER_CARDINAL
     */
    public static int unsignedToSigned(final long x) {
        assert (x >= 0) && (x <= Misc.INTEGER_CARDINAL)
            : "x should be in 0..INTEGER_CARDINAL";
        return x <= Integer.MAX_VALUE
            ? (int) x
            : (int) (x - Misc.INTEGER_CARDINAL);
    } // end method 'unsignedToSigned(long)'

    /**
     * Converts a signed byte value into an unsigned byte value. <br/>
     * Opposite of {@link #unsignedToSignedByte(int)}.
     * @param x value to convert - should be in
     * <tt>{@link java.lang.Byte#MIN_VALUE}..{@link java.lang.Byte#MAX_VALUE}</tt>
     * @return the unsigned value of <tt>x</tt>
     * @see #unsignedToSignedByte(int)
     * @see java.lang.Byte#MIN_VALUE
     * @see java.lang.Byte#MAX_VALUE
     */
    public static int signedToUnsignedByte(final int x) {
        assert (x >= Byte.MIN_VALUE) && (x <= Byte.MAX_VALUE)
            : "x should be in Byte.MIN_VALUE..Byte.MAX_VALUE";
        return x >= 0 ? x : Misc.BYTE_CARDINAL + x;
    } // end method 'signedToUnsignedByte(int)'

    /**
     * Converts an unsigned byte value into a signed byte value. <br/>
     * Opposite of {@link #signedToUnsignedByte(int)}.
     * @param x value to convert - should be in <tt>0..{@link #BYTE_CARDINAL}</tt>
     * @return the signed value of <tt>x</tt>
     * @see #signedToUnsignedByte(int)
     * @see #BYTE_CARDINAL
     */
    public static int unsignedToSignedByte(final int x) {
        assert (x >= 0) && (x <= Misc.BYTE_CARDINAL)
            : "x should be in 0..BYTE_CARDINAL";
        return x <= Byte.MAX_VALUE
            ? x
            : x - Misc.BYTE_CARDINAL;
    } // end method 'unsignedToSignedByte(int)'

    /**
     * Converts a signed short value into an unsigned short value. <br/>
     * Opposite of {@link #unsignedToSignedShort(int)}.
     * @param x value to convert - should be in
     * <tt>{@link java.lang.Short#MIN_VALUE}..{@link java.lang.Short#MAX_VALUE}</tt>
     * @return the unsigned value of <tt>x</tt>
     * @see #unsignedToSignedShort(int)
     * @see java.lang.Short#MIN_VALUE
     * @see java.lang.Short#MAX_VALUE
     */
    public static int signedToUnsignedShort(final int x) {
        assert (x >= Short.MIN_VALUE) && (x <= Short.MAX_VALUE)
            : "x should be in Short.MIN_VALUE..Short.MAX_VALUE";
        return x >= 0 ? x : Misc.SHORT_CARDINAL + x;
    } // end method 'signedToUnsignedShort(int)'

    /**
     * Converts an unsigned short value into a signed short value. <br/>
     * Opposite of {@link #signedToUnsignedShort(int)}.
     * @param x value to convert - should be in <tt>0..{@link #SHORT_CARDINAL}</tt>
     * @return the signed value of <tt>x</tt>
     * @see #signedToUnsignedShort(int)
     * @see #SHORT_CARDINAL
     */
    public static int unsignedToSignedShort(final int x) {
        assert (x >= 0) && (x <= Misc.SHORT_CARDINAL)
            : "x should be in 0..SHORT_CARDINAL";
        return x <= Short.MAX_VALUE
            ? x
            : x - Misc.SHORT_CARDINAL;
    } // end method 'unsignedToSignedShort(int)'

    /**
     * Ensures that a given <tt>long</tt> value can be represented by and
     * <tt>int</tt> and raises a fatal exception if it is not the case.
     * @param v value to test
     * @return <i>v</i> as an <tt>int</tt>
     * @throws Fatal.Exception if <i>v</i> can not be represented as
     *                         an <tt>int</tt>
     */
    public static int ensure32s(final long v) throws Fatal.Exception {
        if ((v >= Integer.MIN_VALUE) && (v <= Integer.MAX_VALUE)) {
            return (int) v;
        } else {
            Fatal.raise64bitElement();
            return 0;
        } // end if/else
    } // end method 'ensure32s(long)'

    /**
     * Converts a character into a byte, according to ISO-8859-1 encoding
     * (equivalent to ISO-Latin-1).
     * @param ch character to convert
     * @return result of conversion
     */
    public static byte convertCharToByte(final char ch) {
        return Misc.CHAR_TO_BYTE[ch - Character.MIN_VALUE];
    } // end method 'convertCharToByte(char)'

    /**
     * Converts a byte into a character, according to ISO-8859-1 encoding
     * (equivalent to ISO-Latin-1).
     * @param by byte to convert
     * @return result of conversion
     */
    public static char convertByteToChar(final byte by) {
        return Misc.BYTE_TO_CHAR[by - Byte.MIN_VALUE];
    } // end method 'convertByteToChar(byte)'

    /**
     * Converts a string into an array of bytes,
     * according to ISO-8859-1 encoding (equivalent to ISO-Latin-1).
     * @param s string to convert - should not be <tt>null</tt>
     * @return result of conversion
     */
    public static byte[] convertStringToBytes(final String s) {
        assert s != null : "null s";
        final byte[] res = new byte[s.length()];
        convertStringToBytes(s, res, 0);
        return res;
    } // end method 'convertStringToBytes(String)'

    /**
     * Converts a string into an array of bytes,
     * according to ISO-8859-1 encoding (equivalent to ISO-Latin-1).
     * @param s string to convert - should not be <tt>null</tt>
     * @param dest destination of conversion - should not be <tt>null</tt><br/>
     *             length should be at least <tt>ofs + s.length()</tt>
     * @param ofs dest offset of first converted character
     */
    public static void convertStringToBytes(final String s,
                                            final byte[] dest,
                                            final int ofs) {
        assert s != null : "null s";
        assert dest != null : "null dest";
        final int len = s.length();
        for (int i = 0; i < len; i++) {
            dest[ofs + i] =
                Misc.CHAR_TO_BYTE[s.charAt(i) - Character.MIN_VALUE];
        } // end for
    } // end method 'convertStringToBytes(String, byte[], int)'

    /**
     * Converts an array of bytes into a string,
     * according to ISO-8859-1 encoding (equivalent to ISO-Latin-1).
     * @param b array to convert - should not be <tt>null</tt>
     * @return result of conversion
     */
    public static String convertBytesToString(final byte[] b) {
        assert b != null : "null b";
        return convertBytesToString(b, 0, b.length);
    } // end method 'convertBytesToString(byte[])'

    /**
     * Converts an array of bytes into a string,
     * according to ISO-8859-1 encoding (equivalent to ISO-Latin-1).
     * @param b array to convert - should not be <tt>null</tt>
     * @param ofs offset of first byte to convert
     * @param len number of bytes to convert
     * @return result of conversion
     */
    public static String convertBytesToString(final byte[] b,
                                              final int ofs,
                                              final int len) {
        assert b != null : "null b";
        final char[] res = new char[len];
        for (int i = 0; i < len; i++) {
            res[i] = Misc.BYTE_TO_CHAR[b[ofs + i] - Byte.MIN_VALUE];
        } // end for
        return new String(res);
    } // end method 'convertBytesToString(byte[], int, int)'

    /**
     * Returns the permissions for the passed file.
     * @param f file to get permissions for - should not be <tt>null</tt>
     * @return the permissions for the passed file, using the Unix convention
     */
    public static int getPermissions(final File f) {
        assert f != null : "null f";
        final int base = (f.canRead() ? Misc.READ_PERM : 0)
            | (f.canWrite() ? Misc.WRITE_PERM : 0)
            | (f.canExecute() ? Misc.EXECUTE_PERM : 0);
        return (base << 6) | (base << 3) | base;
    } // end method 'getPermissions(File)'

    /**
     * Sets the permissions for the passed file.
     * @param f file to set permissions to - should not be <tt>null</tt>
     * @param p permissions, using the Unix convention
     */
    public static void setPermissions(final File f, final int p) {
        assert f != null : "null f";
        final int other = ((p >> 3) & 7) | (p & 7);
        f.setReadable((other & Misc.READ_PERM) != 0, false);
        f.setWritable((other & Misc.WRITE_PERM) != 0, false);
        f.setExecutable((other & Misc.EXECUTE_PERM) != 0, false);
        final int owner = (p >> 6) & 7;
        f.setReadable((owner & Misc.READ_PERM) != 0, true);
        f.setWritable((owner & Misc.WRITE_PERM) != 0, true);
        f.setExecutable((owner & Misc.EXECUTE_PERM) != 0, true);
    } // end method 'setPermissions(File, int)'

} // end class 'Misc'
